package xj.graph2d;

import static xj.graph2d.DrawShapeConstants.PointOutside;
import static xj.graph2d.DrawShapeConstants.WRITE_XML_DEFAULT;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.JOptionPane;

import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import xj.app.Application;
import xj.lang.CodeWriter;
import xj.util.gui.Rectangle2DFloat;
import xj.util.xml.XMLUtil;

import static xj.app.BaseApplicationFrame.logInfo;

public class SimpleDrawDocument 
  extends Document 
  implements DrawDocument,  DocumentConstants {

  public SimpleDrawDocument() {
    this(null);
  }

  public SimpleDrawDocument(Application application) {
    super(application);
    creationTime = lastModificationTime = System.currentTimeMillis();
  }

  public SimpleDrawDocument makeDocument() { 
    return new SimpleDrawDocument(application);
  }

  public SimpleDrawView makeView() { 
    return new SimpleDrawView(this);
  }

  /*
   * Basic methods for manipulating a list of shapes.
   */

  public final int count() {
    return shapes.size();
  }

  public void add(DrawShape s) {
    add(s, false);
  }

  public void add(DrawShape s, boolean sel) {
    shapes.add(s);
    if (sel) {
      selected = s;
    }
    lastAdded = s;
    if (listener != null) {
      listener.shapeAdded(s);
    }
  }

  public void add(int i, DrawShape s) {
    add(i, s, false);
  }

  public void add(int i, DrawShape s, boolean sel) {
    if (s != null) {
      int n = shapes.size();
      if (i < 0) {
	i = 0;
      }
      if (i > n) {
	i = n;
      }
      shapes.add(i, s);
      if (sel) {
	selected = s;
      }
      lastAdded = s;
      if (listener != null) {
	listener.shapeAdded(s);
      }
    }
  }

  public int find(DrawShape s) {
    if (s != null) {
      int n = shapes.size();
      for (int i = 0; i < n; i++) {
	if (s == shapes.get(i)) {
	  return i;
	}
      }
    }
    return -1;
  }

  public DrawShape findInRole(int role) {
    if (shapes != null) {
      int n = shapes.size();
      for (int i = 0; i < n; i++) {
	DrawShape s = shapes.get(i);
	if (s.getRole() == role) {
	  return s;
	}
      }
    }
    return null;
  }

  public void remove(DrawShape s) {
    if (s != null) {
      int n = shapes.size();
      for (int i = 0; i < n; i++) {
	if (s == shapes.get(i)) {
	  shapes.remove(i);
	  if (listener != null) {
	    listener.shapeRemoved(s);
	  }
	  break;
	}
      }
    }
  }

  public void remove(int i) {
    if (listener != null) {
      DrawShape s = shapes.get(i);
      if (listener != null) {
	listener.shapeRemoved(s);
      }
    }
    shapes.remove(i);
  }

  public void removeAll() {
    shapes.clear();
    group = null;
    if (listener != null) {
      listener.refresh();
    }
  }

  public DrawShape get(int i) {
    if (i >= 0 && i < shapes.size()) {
      DrawShape shape = (DrawShape) shapes.get(i);
      /*
       * if (selected == shape) { selected = null; }
       */
      return shape;
    } else {
      return null;
    }
  }

  public List<DrawShape> getShapes() {
    return shapes;
  }

  public Iterator<DrawShape> iterator() {
    return shapes.iterator();
  }

  public DrawShape findSubshape(Predicate pred) {
    return DrawShapeBase.findSubshape(pred, false, shapes);
  }

  public DrawShape findSubshape(Predicate pred, boolean recurse) {
    return DrawShapeBase.findSubshape(pred, recurse, shapes);
  }

  public List<DrawShape> findAllSubshapes(Predicate pred) {
    return DrawShapeBase.findAllSubshapes(pred, false, shapes, null);
  }

  public List<DrawShape> findAllSubshapes(Predicate pred, boolean recurse) {
    return DrawShapeBase.findAllSubshapes(pred, recurse, shapes, null);
  }

  /*
   * Notification methods
   */

  public void shapeChanged(DrawShape s) {
    if (s != null && listener != null) {
      listener.shapeChanged(s);
    }
  }

  public void shapeAdded(DrawShape s) {
    if (s != null &&
	listener != null) {
      listener.shapeAdded(s);
    }
  }

  public void shapeRemoved(DrawShape s) {
    if (s != null && listener != null) {
      listener.shapeRemoved(s);
    }
  }

  public void refresh() {
    if (listener != null) {
      listener.refresh();
    }
  }

  /*
   * Methods for handling the (unique) selected shape of the document.
   */  
  public DrawShape getSelectedShape() {
    return selected;
  }

  public void setSelectedShape(DrawShape s) {
    setSelectedShape(s, true);
  }

  public void setSelectedShape(DrawShape s, boolean update) {
    // System.out.println("SimpleDrawDocument.setSelectedShape() s=" + s);
    selected = s;
    if (update && listener != null) {
      listener.shapeSelected(s);
    }
  }

  /*
   * Methods for handling multiple selections in the document
   */
  public void flipSelection(DrawShape s) {
    if (s != null) {
      if (debug) {
	System.out.println("SimpleDrawDocument.flipSelection()");
      }

      if (selectionSet.contains(s)) {
	selectionSet.remove(s);
      } else {
	selectionSet.add(s);
      }

      if (debug) {
	System.out.println("SimpleDrawDocument.flipSelection() #=" + 
			   selectionSet.size());
      }
    }
    if (listener != null) {
      listener.selectionSetUpdated();
    }
  }

  public void addSelection(DrawShape s) {
    if (s != null) {
      selectionSet.add(s);
    }
    if (listener != null) {
      listener.selectionSetUpdated();
    }
  }

  public boolean inSelectionSet(DrawShape s) {
    return selectionSet.contains(s);
  }

  public void selectionSetUpdated() {
    if (listener != null) {
      listener.selectionSetUpdated();
    }
  }

  public void clearSelectionSet() {
    selectionSet.clear();
    if (listener != null) {
      listener.selectionSetUpdated();
    }
  }

  public Set<DrawShape> getSelectionSet() {
    return selectionSet;
  }

  public DrawShape getLastAddedShape() {
    return lastAdded;
  }

  /*
   * Methods for handling the selected group of the document
   */
  public Group getGroup() {
    return group;
  }

  public void setGroup(Group g) {
    setGroup(g, false);
  }

  public void setGroup(Group g, boolean check) {
    if (debug) {
      System.out.println("SimpleDrawDocument.setGroup()");
    }

    if (check) {
      // remove the shapes in the group from the document
      Iterator iter = g.allShapes();
      while (iter.hasNext()) {
	DrawShape shape = (DrawShape) iter.next();
	if (shape != null && !shape.isZeroSize()) {
	  int n = shapes.size();
	  for (int i = 0; i < n; i++) {
	    if (shape == shapes.get(i)) {
	      shapes.remove(i);
	      break;
	    }
	  }
	}
      }
    }
    group = g;
    if (listener != null) {
      listener.refresh();
      listener.shapeSelected(group);
    }
  }

  public void makeGroup(List gshapes, boolean fit) {
    if (debug) {
      System.out.println("SimpleDrawDocument.makeGroup(List)");
    }

    if (gshapes != null && gshapes.size() > 0) {
      Group g = new RectangularGroup();
      Iterator iter = gshapes.iterator();
      while (iter.hasNext()) {
	DrawShape shape = (DrawShape) iter.next();
	if (shape != null && !shape.isZeroSize()) {
	  int n = shapes.size();
	  for (int i = 0; i < n; i++) {
	    if (shape == shapes.get(i)) {
	      shapes.remove(i);
	      shape.setSearchable(false);
	      g.addShape(shape);

	      /*
	       * if (listener != null) { listener.shapeRemoved(shape); }
	       */
	      break;
	    }
	  }
	}
      }
      if (fit) {
	g.fitComponents();
      }
      group = g;
      if (listener != null) {
	listener.refresh();
	listener.shapeSelected(group);
      }
    }
  }

  public void makeGroup(Group g, boolean fit) {
    if (debug) {
      System.out.println("SimpleDrawDocument.makeGroup(Group)");
    }

    if (g != null && g instanceof RectangularGroup) {
      group = g;
      Iterator iter = shapes.iterator();
      while (iter.hasNext()) {
	DrawShape shape = (DrawShape) iter.next();
	if (shape != null && !shape.isZeroSize()) {
	  if (group.contains(shape)) {
	    shape.setSearchable(false);
	    group.addShape(shape);
	    iter.remove();

	    /*
	     * if (listener != null) { listener.shapeRemoved(shape); }
	     */
	  }
	}
      }
      if (fit) {
	group.fitComponents();
      }
      if (listener != null) {
	listener.refresh();
	listener.shapeSelected(group);
      }
    }
  }

  public void selectGroup(Group g) {
    if (g != null && listener != null) {
      listener.shapeSelected(group);
    }
  }

  /* return the previous group */
  public Group ungroup() {
    Group g = group;
    if (g != null) {
      ungroup(g);
      group = null;
      if (listener != null) {
	listener.refresh();
      }
    }
    return g;
  }

  public void ungroup(Group g) {
    ungroup(g, false);
  }

  public void ungroup(Group g, boolean remove) {
    if (debug) {
      System.out.println("SimpleDrawDocument.ungroup()");
    }

    if (g != null) {
      ListIterator<DrawShape> iter = g.allShapes();
      while (iter.hasNext()) {
	DrawShape shape = iter.next();
	if (shape != null) {
	  shape.setSearchable(true);
	  shape.setParent(null);
	  //shapes.add(shape);
	  add(shape);
	  if (remove) {
	    iter.remove();
	  }

	  /*
	   * if (listener != null) { listener.shapeAdded(shape); }
	   */
	}
      }
      g.deactivate();
    }
  }

  public Rectangle2D getBounds() {
    int n = shapes.size();
    Rectangle2D r = new Rectangle2DFloat();
    if (n > 0) {
      DrawShape shape; // = (DrawShape) shapes.get(0);
      boolean first = true;
      for (int i = 0; i < n; i++) {
	shape = shapes.get(i);
	Rectangle2D r1 = shape.getBounds();

	if (debug) {
	  System.out.println("SimpleDrawDocument.getBounds() r[" + i + "]="
			     + shape.getBounds());
	}

	if (first) {
	  r.setRect((float) r1.getX(), (float) r1.getY(), 
		    (float) r1.getWidth(), (float) r1.getHeight());
	  first = false;
	} else {
	  Rectangle2D.union(r, r1, r);
	}
      }
    }

    if (r != null) {
      if (padDocumentBound) {
	// To ensure the whole figures are visible
	float d = 10f; // 3.0f;
	r.setRect((float) r.getX() - d, (float) r.getY() - d, 
		  (float) r.getWidth() + 2 * d + 1, (float) r.getHeight() + 2 * d + 1);
      }

      if (debug) {
	System.out.println("SimpleDrawDocument.getBounds() result=" + r);
      }

      return r;
    } else {
      return new Rectangle2DFloat();
    }
  }

  public void moveAll(float dx, float dy) { 
    int n = shapes.size();
    if (n > 0) {
      for (int i = 0; i < n; i++) {
	DrawShape shape = shapes.get(i);
	shape.move(dx, dy);
      }
    }
  }

  public DrawShape search(Point2D p, DrawShape exclude) {
    int position = PointOutside;
    if (group != null) {
      position = group.isInside(p);
      if (position != PointOutside && group != exclude) {
	return group;
      }
    }

    List matches = DrawShapeSearch.searchAll(iterator(), p, exclude);
    if (matches != null) {
      int n = matches.size();
      for (int i = 0; i < n; i++) {
	DrawShapeSearch.Result r = (DrawShapeSearch.Result) matches.get(i);
	DrawShape shape = r.shape;
	while (shape != null) {
	  if (shape != exclude && 
	      !shape.isDescendantOf(exclude)) {
	    return shape;
	  } else {
	    shape = shape.getParent();
	  }
	}
      }
    }
    return null;
  }

  public DrawShapeSearch.Result search(Point2D p) {
    int position = PointOutside;
    if (group != null) {
      position = group.isInside(p);
      if (position != PointOutside) {
	return new DrawShapeSearch.Result(group, position);
      }
    }

    return DrawShapeSearch.search(iterator(), p, DrawShapeSearch.BEST_MATCH);
  }

  public boolean isOutside(Point2D p) {
    int n = shapes.size();
    for (int i = 0; i < n; i++) {
      DrawShape s = shapes.get(i);
      if (!s.isOutside(p)) { 
	return false; 
      }
    }
    if (group != null) {
      if (!group.isOutside(p)) { 
	return false; 
      }
    }
    return true; 
  }

  public UndoManager getUndoManager() {
    return undoManager;
  }

  public void resetUndoManager() {
    if (undoManager != null) {
      undoManager.reset();
    }
  }

  public void addCommand(Command command) {
    if (command != null) {
      undoManager.addCommand(command);
    }
  }

  public void executeCommand(Command command) {
    if (command != null) {
      undoManager.executeCommand(command);
    }
  }

  public void undoCommand() {
    undoManager.undoCommand();
  }

  public void redoCommand() {
    undoManager.redoCommand();
  }

  public void revokeLastCommand() {
    undoManager.revokeLastCommand();
  }

  /**
   * clear and reset the current document/view
   */
  public void newFile() {
    title = null;
    description = null;
    resetUndoManager();

    setChanged();
    removeAll();
    // shapes.clear();
    setCanvasWidth(DEFAULT_CANVAS_WIDTH);
    setCanvasHeight(DEFAULT_CANVAS_HEIGHT);
    notifyObservers("canvas dimension");
    clearChanged();

    creationTime = System.currentTimeMillis();
  }

  public void closeFile() {

  }

  static public String getNameFromFilename(String filename) {
    if (filename != null) {
      char c = File.separatorChar;
      int i = filename.lastIndexOf(c);
      if (i >= 0) {
	filename = filename.substring(i + 1);
      }
      i = filename.lastIndexOf('.');
      if (i >= 0) {
	filename = filename.substring(0, i);
      }
      return filename;
    }
    return null;
  }

  public void openFile(String filename, int filetype) 
    throws IOException, DocumentException {
    this.filename = filename;
    this.name = getNameFromFilename(filename);
  }

  public boolean saveFile() 
    throws IOException, DocumentException {
    if (filename != null) {
      return saveFile(filename, filetype, false);
    }
    return false;
  }

  public boolean saveFile(String filename, int filetype, boolean checkExists)
    throws IOException, DocumentException {
    return false;
  }

  public void autoSaveFile() {     
    if (autoSaveEnabled && 
	isDocumentChanged()) { 
      try { 
	String autofilename = autoSaveFileName(); 

	//System.out.println("SimpleDrawDocument.autoSaveFile() " + autofilename);
	logInfo("SimpleDrawDocument.autoSaveFile() " + autofilename);

	saveXMLFile(autofilename);
      } catch (IOException e) {
      } catch (DocumentException e) {
      }
    }
  }

  protected String autoSaveFileName() {  
    if (filename != null) { 
      return filename + "~"; 
    } else { 
      return "autosave.xml~";
    }
  }

  public void openSerializedFile(String filename) 
    throws IOException, ClassNotFoundException {
    ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
    shapes = (List) in.readObject();
    in.close();

    this.filename = filename;
    this.name = getNameFromFilename(filename);

    if (listener != null) {
      listener.refresh();
    }
    isNew = false;
  }

  public void saveSerializedFile(String filename) 
    throws IOException {
    lastModificationTime = System.currentTimeMillis();
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
    out.writeObject(shapes);
    out.close();
    System.out.println("Save drawing to " + filename);
  }

  public boolean openXMLFile(String filename) 
    throws IOException, DocumentException {

    if (debug) {
      System.out.println("openXMLFile(): filename=" + filename);
    }

    removeAll();

    try {
      // org.w3c.dom.Document xmldoc = XMLUtil.createXMLDocument(filename,
      // false);
      org.w3c.dom.Document xmldoc = XMLUtil.parseXMLDocument(filename, false);
      if (xmldoc != null) {
	if (debug) {
	  System.out.println("openXMLFile(): create XML document success");
	}

	String uri0 = null;
	if (strictNamespace && 
	    namespaceURI != null && 
	    namespaceURI.length() > 0) {
	  uri0 = namespaceURI;
	}

	org.w3c.dom.Element docElement = xmldoc.getDocumentElement();
	String uri;
	uri = docElement.getNamespaceURI();
	if ((uri0 == null || uri0.equals(uri)) && 
	    "Document".equals(docElement.getLocalName())) {

	  String type = docElement.getAttribute("type");
	  setDocumentType(toDocumentType(type));

	  if (debug) {
	    System.out.println("openXMLFile(): <Document> tag");
	  }

	  org.w3c.dom.NodeList nodes = docElement.getChildNodes();
	  if (nodes != null) {
	    int n = nodes.getLength();
	    for (int i = 0; i < n; i++) {
	      org.w3c.dom.Node node = nodes.item(i);
	      uri = node.getNamespaceURI();
	      if (uri0 == null || uri0.equals(uri)) {
		if ("Properties".equals(node.getLocalName())) {
		  processDocumentProperties(node, uri0);
		} else if ("TimeStamp".equals(node.getLocalName())) {
		  processDocumentTimeStamp(node, uri0);
		} else if ("Settings".equals(node.getLocalName())) {
		  processDocumentSettings(node, uri0);
		} else if ("Attributes".equals(node.getLocalName())) {
		  processDocumentAttributes(node, uri0);
		} else if ("Shapes".equals(node.getLocalName())) {
		  processDocumentShapes(node, uri0);
		} else {
		  if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
		    processDocumentChild(node, uri0);
		  }
		}
	      }
	    }
	  }
	}

	this.filename = filename;
	this.name = getNameFromFilename(filename);

	if (listener != null) {
	  listener.refresh();
	}

	isNew = false;
	return true;
      } else {
	return false;
      }
    } catch (SAXParseException e) {
      throw new DocumentException("Error in loading file " + filename + ": " + 
				  e.getMessage(), e);
    } catch (SAXException e) {
      throw new DocumentException("Error in loading file " + filename + ": " + 
				  e.getMessage(), e);
    }
  }

  protected void processDocumentProperties(org.w3c.dom.Node node, String uri0) {
    if (node != null && "Properties".equals(node.getLocalName())) {

      if (debug) {
	System.out.println("openXMLFile(): <Properties> tag");
      }

      org.w3c.dom.NodeList propNodes = node.getChildNodes();
      if (propNodes != null) {
	int m = propNodes.getLength();
	for (int j = 0; j < m; j++) {
	  org.w3c.dom.Node pnode = propNodes.item(j);
	  String uri = pnode.getNamespaceURI();
	  if (pnode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE && 
	      (uri0 == null || uri0.equals(uri))) {
	    String pname = pnode.getLocalName();
	    org.w3c.dom.Node tnode = pnode.getFirstChild();
	    String text = null;
	    if (tnode != null && 
		(tnode.getNodeType() == org.w3c.dom.Node.TEXT_NODE || 
		 tnode.getNodeType() == org.w3c.dom.Node.CDATA_SECTION_NODE)) {
	      text = tnode.getNodeValue();
	    }
	    if ("Author".equals(pname)) {
	      author = text;
	    } else if ("Title".equals(pname)) {
	      title = text;
	    } else if ("Description".equals(pname)) {
	      description = text;
	    } else if ("Organization".equals(pname)) {
	      organization = text;
	    }
	  }
	}
      }
    }
  }

  protected void processDocumentTimeStamp(org.w3c.dom.Node node, String uri0) {
    if (node != null && "TimeStamp".equals(node.getLocalName())) {
      if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
	org.w3c.dom.Element e = (org.w3c.dom.Element) node;
	String ctime = e.getAttribute("c");
	String mtime = e.getAttribute("m");
	try {
	  creationTime = Long.parseLong(ctime);
	} catch (NumberFormatException ex) {
	}
	try {
	  lastModificationTime = Long.parseLong(mtime);
	} catch (NumberFormatException ex) {
	}
      }
    }
  }

  protected void processDocumentSettings(org.w3c.dom.Node node, String uri0) {
    if (node != null && 
	"Settings".equals(node.getLocalName())) {
      if (debug) {
	System.out.println("openXMLFile(): <Settings> tag");
      }

      org.w3c.dom.NodeList settingsNodes = node.getChildNodes();
      if (settingsNodes != null) {
	int m = settingsNodes.getLength();
	for (int j = 0; j < m; j++) {
	  org.w3c.dom.Node snode = settingsNodes.item(j);
	  String uri = snode.getNamespaceURI();
	  if (snode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE && 
	      (uri0 == null || uri0.equals(uri))) {
	    String sname = snode.getLocalName();

	    if (debug) {
	      System.out.println("openXMLFile(): <" + sname + "> tag");
	    }

	    org.w3c.dom.Element e = (org.w3c.dom.Element) snode;
	    if ("Canvas".equals(sname)) {
	      float w = DEFAULT_CANVAS_WIDTH;
	      float h = DEFAULT_CANVAS_HEIGHT;
	      try {
		String s = e.getAttribute("width");
		w = Float.parseFloat(s);
	      } catch (NumberFormatException ex) {
	      }
	      try {
		String s = e.getAttribute("height");
		h = Float.parseFloat(s);
	      } catch (NumberFormatException ex) {
	      }
	      int u = UNIT_PIXEL;
	      try {
		String s = e.getAttribute("unit");
		u = Integer.parseInt(s);
	      } catch (NumberFormatException ex) {
	      }
	      setUnit(u);
	      setCanvasWidth(w);
	      setCanvasHeight(h);
	      setChanged();
	      notifyObservers("canvas dimension");
	      clearChanged();
	    } else if ("Margin".equals(sname)) {
	      try {
		String s = e.getAttribute("width");
		float w = Float.parseFloat(s);
		setMarginWidth(w);
		//System.out.println("openXMLFile(): <Margin> w=" + w);
	      } catch (NumberFormatException ex) {
		//System.err.println(ex.getMessage());
		setMarginWidth(DEFAULT_MARGIN);
	      }
	      try {
		String s = e.getAttribute("height");
		float h = Float.parseFloat(s);
		setMarginHeight(h);
		//System.out.println("openXMLFile(): <Margin> h=" + h);
	      } catch (NumberFormatException ex) {
		//System.err.println(ex.getMessage());
		setMarginHeight(DEFAULT_MARGIN);
	      }
	    }
	    //System.out.println("openXMLFile(): done <" + sname + "> tag");
	  }
	  
	}
      }
    }
  }

  protected void processDocumentAttributes(org.w3c.dom.Node node, String uri0) {
    if (node != null && 
	"Attributes".equals(node.getLocalName())) {
      if (node.hasChildNodes()) {
	org.w3c.dom.NodeList nodes = node.getChildNodes();
	if (nodes != null) {
	  int n = nodes.getLength();
	  for (int i = 0; i < n; i++) {
	    org.w3c.dom.Node anode = nodes.item(i);
	    if (anode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
	      org.w3c.dom.Element e = (org.w3c.dom.Element) anode;
	      String aname = e.getLocalName();
	      if ("Attribute".equals(aname)) {
		String key = e.getAttribute("key");
		String value = e.getAttribute("value");
		attr.put(key, value);
	      }
	    }
	  }
	}
      }
    }
  }

  protected void processDocumentShapes(org.w3c.dom.Node node, String uri0) {
    if (node != null && "Shapes".equals(node.getLocalName())) {
      if (debug) {
	System.out.println("openXMLFile(): <Shapes> tag");
      }

      org.w3c.dom.NodeList shapeNodes = node.getChildNodes();
      if (shapeNodes != null) {
	int m = shapeNodes.getLength();
	// shapes = new ArrayList(m);
	for (int j = 0; j < m; j++) {
	  org.w3c.dom.Node snode = shapeNodes.item(j);
	  String uri = snode.getNamespaceURI();
	  if (snode.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE && 
	      (uri0 == null || uri0.equals(uri))) {

	    if (debug) {
	      System.out.println("openXMLFile(): <" + snode.getNodeName() + "> tag");
	    }

	    DrawShape shape = DrawShapeBase.makeShapeFromXML((org.w3c.dom.Element) snode, uri0);
	    if (shape != null) {
	      shapes.add(shape);
	      lastAdded = shape;
	    }
	  }
	}
      }
    }
  }

  protected void processDocumentChild(org.w3c.dom.Node node, String uri0) {
  }

  public boolean saveSerializedFile(String filename, boolean checkExists)
    throws IOException {
    // try {
    if (checkExists) {
      if (!checkWriteFile(filename)) {
	return false;
      }
    }
    saveSerializedFile(filename);
    return true;

    /*
     * } catch (IOException e) { System.out.println("Unable to write file: " +
     * filename); e.printStackTrace(); } return false;
     */
  }

  public boolean saveXMLFile(String filename, boolean checkExists)
    throws IOException, DocumentException {
    // try {
    if (checkExists) {
      if (!checkWriteFile(filename)) {
	return false;
      }
    }
    saveXMLFile(filename);
    return true;

    /*
     * } catch (IOException e) { System.out.println("Unable to write file: " +
     * filename); e.printStackTrace(); } catch (Exception e) {
     * System.out.println("Unable to write file: " + filename);
     * e.printStackTrace(); } return false;
     */
  }

  protected boolean checkWriteFile(String filename) {
    if (filename != null) {
      File file = new File(filename);
      if (file.exists()) {
	int result = JOptionPane.showConfirmDialog(null, "The file " + filename
						   + " already exists.\n"
						   + "Do you want to overwrite the existing file?",
						   "Overwrite the existing file?", 
						   JOptionPane.YES_NO_OPTION);
	if (result != JOptionPane.YES_OPTION) {
	  return false;
	}
      }
    }
    return true;
  }

  public void saveXMLFile(String filename) 
    throws IOException, DocumentException {
    if (debug) {
      System.out.println("SimpleDrawDocument.saveXMLFile()");
    }

    lastModificationTime = System.currentTimeMillis();
    OutputStream out = new FileOutputStream(filename);
    PrintWriter pout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, "UTF8")));
    CodeWriter cout = new CodeWriter(pout);
    cout.setIndentSize(1);
    writeXML(cout);
    cout.close();
  }

  public void writeXML(CodeWriter out) throws IOException, DocumentException {
    writeXML(out, writeXMLMode);
  }

  public void writeXML(CodeWriter out, int mode) throws IOException,
      DocumentException {
    if (out != null) {
      try {
	out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?>");
	writeXMLDocumentHeader(out);
	writeXMLDocumentProperties(out);
	writeXMLDocumentShapes(out, mode, useDefaultNamespace ? null : namespacePrefix);
	out.thisIndentDec();
	writeXMLDocumentFooter(out);
      } catch (Exception e) {
	throw new DocumentException("Error in saving document", e);
      }
    }
  }

  public void writeXMLDocumentHeader(CodeWriter out) {
    if (out != null) {
      String appName = getAppName();
      if (appName != null) {
	out.println("<!-- Created by " + appName + " " + (new Date()) + " -->");
      }
      String version = getVersion();
      String rdate = getReleaseDate();
      if (version == null) {
	version = "3.1";
      }
      if (rdate != null) {
	version += ("/" + rdate);
      }
      String docname = DrawShapeBase.getPrefixedName("Document",
						     useDefaultNamespace ? null : namespacePrefix);
      out.print("<" + docname);
      if (namespaceURI != null && namespaceURI.length() > 0) {
	out.print(" xmlns");
	if (!useDefaultNamespace && namespacePrefix != null && namespacePrefix.length() > 0) {
	  out.print(":" + namespacePrefix);
	}
	out.print("=\"" + namespaceURI + "\"");
      }
      out.print(" type=\"" + doctype + "\"");
      out.println(" version=\"" + version + "\">");
      out.thisIndentInc();

      String tsname = DrawShapeBase.getPrefixedName("TimeStamp",
						    useDefaultNamespace ? null : namespacePrefix);
      out.println("<" + tsname + " c=\"" + creationTime + 
		  "\" m=\"" + lastModificationTime + "\">");
      out.thisIndentInc();
      String nodename = 
	DrawShapeBase.getPrefixedName("Created",
				      useDefaultNamespace ? null : namespacePrefix);
      out.println("<" + nodename + ">" + 
		  xj.util.xml.XSLTUtil.filterXMLText((new Date(creationTime)).toString()) + 
		  "</" + nodename + ">");
      nodename = DrawShapeBase.getPrefixedName("LastModified",
					       useDefaultNamespace ? null : namespacePrefix);
      out.println("<" + nodename + ">" + 
		  xj.util.xml.XSLTUtil.filterXMLText((new Date(lastModificationTime)).toString()) + 
		  "</" + nodename + ">");
      out.thisIndentDec();
      out.println("</" + tsname + ">");
    }
  }

  public void writeXMLDocumentProperties(CodeWriter out) {
    if (out != null) {
      // the document properties
      if (author != null || title != null || description != null
	  || organization != null) {
	String propname = 
	  DrawShapeBase.getPrefixedName("Properties",
					useDefaultNamespace ? null : namespacePrefix);
	out.println("<" + propname + ">");
	out.thisIndentInc();
	String nodename;
	if (author != null) {
	  nodename = DrawShapeBase.getPrefixedName("Author",
						   useDefaultNamespace ? null : namespacePrefix);
	  out.println("<" + nodename + ">" + 
		      xj.util.xml.XSLTUtil.filterXMLText(author) + 
		      "</" + nodename + ">");
	}
	if (title != null) {
	  nodename = DrawShapeBase.getPrefixedName("Title",
						   useDefaultNamespace ? null : namespacePrefix);
	  out.println("<" + nodename + ">" + 
		      xj.util.xml.XSLTUtil.filterXMLText(title) + 
		      "</" + nodename + ">");
	}
	if (description != null) {
	  nodename = DrawShapeBase.getPrefixedName("Description",
						   useDefaultNamespace ? null : namespacePrefix);
	  out.println("<" + nodename + ">" + 
		      xj.util.xml.XSLTUtil.filterXMLText(description) + 
		      "</" + nodename + ">");
	}
	if (organization != null) {
	  nodename = DrawShapeBase.getPrefixedName("Organization",
						   useDefaultNamespace ? null : namespacePrefix);
	  out.println("<" + nodename + ">" + 
		      xj.util.xml.XSLTUtil.filterXMLText(organization) + 
		      "</" + nodename + ">");
	}

	out.thisIndentDec();
	out.println("</" + propname + ">");
      }

      // the settings
      String settingname = 
	DrawShapeBase.getPrefixedName("Settings",
				      useDefaultNamespace ? null : namespacePrefix);
      out.println("<" + settingname + ">");
      out.thisIndentInc();

      String nodename = 
	DrawShapeBase.getPrefixedName("Canvas",
				      useDefaultNamespace ? null : namespacePrefix);
      out.println("<" + nodename + " unit=\"" + unit + "\" width=\"" + canvasWidth + 
		  "\" height=\"" + canvasHeight + "\"/>");

      nodename = DrawShapeBase.getPrefixedName("Margin",
					       useDefaultNamespace ? null : namespacePrefix);
      out.println("<" + nodename + " width=\"" + marginWidth + 
		  "\" height=\"" + marginHeight + "\"/>");

      out.thisIndentDec();
      out.println("</" + settingname + ">");

     if (attr.size() > 0) { 
	nodename = DrawShapeBase.getPrefixedName("Attributes", namespacePrefix);
	out.println("<" + nodename + ">");
	out.thisIndentInc();
	for (Map.Entry<String,String> e : attr.entrySet()) { 
	  String atag = DrawShapeBase.getPrefixedName("Attribute", namespacePrefix);
	  out.println("<" + atag + " key=\"" + e.getKey() + "\" value=\"" + e.getValue() + "\"/>");
	}
	out.thisIndentDec();
	out.println("</" + nodename + ">");
      }
    }
  }

  public void writeXMLDocumentFooter(CodeWriter out) {
    if (out != null) {
      String docname = DrawShapeBase.getPrefixedName("Document",
						     useDefaultNamespace ? null : namespacePrefix);
      out.println("</" + docname + ">");
    }
  }

  public void writeXMLDocumentShapes(CodeWriter out, String namespacePrefix) {
    writeXMLDocumentShapes(out, WRITE_XML_DEFAULT, namespacePrefix);
  }

  public void writeXMLDocumentShapes(CodeWriter out, int mode,
				     String namespacePrefix) {
    if (out != null) {
      String nodename = DrawShapeBase.getPrefixedName("Shapes", namespacePrefix);
      if (shapes != null) {
	ungroup(); // all shapes in the current group will be properly written 

	out.println("<" + nodename + ">");
	out.thisIndentInc();
	int n = shapes.size();
	for (int i = 0; i < n; i++) {
	  DrawShape shape = (DrawShape) shapes.get(i);
	  if (shape != null) {
	    shape.writeXML(out, mode, namespacePrefix);
	  }
	}
	if (group != null) {
	  Iterator iter = group.allShapes();
	  while (iter.hasNext()) {
	    DrawShape shape = (DrawShape) iter.next();
	    if (shape != null) {
	      shape.writeXML(out, mode, namespacePrefix);
	    }
	  }
	}
	out.thisIndentDec();
	out.println("</" + nodename + ">");
      } else {
	out.println("<" + nodename + "/>");
      }
    }
  }

  protected static final String tempname = "temp"; 

  public SimpleDrawDocument cloneDocument() { 
    SimpleDrawDocument doc = makeDocument();
    //String tempfile = application.getAppDir() + File.separator + "temp" + File.separator + "temp" + 
    String tempfile = "temp" + (System.currentTimeMillis() % 10000);
    
    if (debug) { 
      System.out.println("SimpleDrawDocument.cloneDocument() tempfile=" + tempfile);
    }

    try { 
      saveXMLFile(tempfile);
      doc.openXMLFile(tempfile);
      //File file = new File(tempfile);
      //file.delete();
    } catch (IOException e) {
      e.printStackTrace();
    } catch (DocumentException e) {
      e.printStackTrace();
    }
    return doc; 
  }

  public String getAuthor() {
    return author;
  }

  public void setAuthor(String author) {
    this.author = author;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getDescription() {
    return description;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public String getOrganization() {
    return organization;
  }

  public void setOrganization(String organization) {
    this.organization = organization;
  }

  public long getCreationTime() {
    return creationTime;
  }

  public long getLastModificationTime() {
    return lastModificationTime;
  }

  public void setFilename(String filename) {
    this.filename = filename;
  }

  public String getFilename() {
    return filename;
  }

  public void setFiletype(int filetype) {
    this.filetype = filetype;
  }

  public int getFiletype() {
    return filetype;
  }

  public boolean isAutoSaveEnabled() { 
    return autoSaveEnabled;
  }

  public void setAutoSaveEnabled(boolean b) { 
    autoSaveEnabled = b;
  }

  public boolean isEditable() { 
    return editable;
  }

  public void setEditable(boolean editable) { 
    this.editable = editable;
  }

  public void setNamespace(String namespaceURI, String namespacePrefix) {
    this.namespaceURI = namespaceURI;
    this.namespacePrefix = namespacePrefix;
  }

  public String getNamespaceURI() {
    return namespaceURI;
  }

  public String getNamespacePrefix() {
    return namespacePrefix;
  }

  public int getWriteXMLMode() {
    return writeXMLMode;
  }

  public void setWriteXMLMode(int writeXMLMode) {
    this.writeXMLMode = writeXMLMode;
  }

  public boolean isStrictNamespace() {
    return strictNamespace;
  }

  public void setStrictNamespace(boolean strictNamespace) {
    this.strictNamespace = strictNamespace;
  }

  public Map<String, String> getDocumentAttributes() { 
    return attr;
  }

  public String getDocumentAttribute(String key) { 
    if (key != null) { 
      return attr.get(key); 
    }
    return null;
  }

  public void setDocumentAttribute(String key, String value) { 
    if (key != null) { 
      if (value != null) { 
	attr.put(key, value); 
      } else { 
	attr.remove(key);
      }
    }
  }

  public boolean isDocumentNew() {
    return isNew;
  }

  public boolean isDocumentChanged() {
    return undoManager.isChanged();
  }

  public boolean isDocumentSaved() {
    return saved;
  }

  public void documentSaved() {
    undoManager.save();
    application.setChanged(undoManager.isChanged());
    saved = true;
  }

  public void dump() {
    System.out.println("SimpleDrawDocument shapes dump begin =======");
    int n = shapes.size();
    for (int i = 0; i < n; i++) {
      DrawShape shape = (DrawShape) shapes.get(i);
      dumpShape(shape, "" + i);
    }
    System.out.println("SimpleDrawDocument shapes dump end =========");
  }

  public void dumpShape(DrawShape shape, String id) { 
    if (shape != null) { 
      System.out.println("shape[" + id + "]=@" + 
			 Integer.toHexString(shape.hashCode()) + " " +shape);
      if (shape.getShapeCount() > 0) {
	int i = 0; 
	Iterator iter = shape.allShapes();
	while (iter.hasNext()) {
	  DrawShape ss = (DrawShape) iter.next();
	  //System.out.println("  subshape:" + ss);
	  dumpShape(ss, id + "." + i++);
	}
      }
    }
  }

  protected List<DrawShape> shapes = new ArrayList<DrawShape>();

  // protected BitSet selectionSet = new BitSet();
  protected Set<DrawShape> selectionSet = new HashSet<DrawShape>();

  protected DrawShape selected = null;

  protected DrawShape lastAdded = null; 

  protected Group group = null;

  protected UndoManager undoManager = new UndoManager();

  protected String author;

  protected String title;

  protected String description;

  protected String organization;

  protected long creationTime;

  protected long lastModificationTime;

  protected String filename;

  protected int filetype;

  protected boolean autoSaveEnabled = true;

  protected boolean editable = true;

  protected boolean saved = false;

  protected boolean isNew = true;

  protected int writeXMLMode = WRITE_XML_DEFAULT;

  protected String namespaceURI = null;

  protected String namespacePrefix = null;

  protected boolean strictNamespace = false;

  protected boolean padDocumentBound = false;

  // custom attributes
  protected Map<String, String> attr = new HashMap<String, String>();

  protected static boolean useDefaultNamespace = true;
  
  protected static final boolean debug = false;

}
